﻿Option Strict On
Option Explicit On
Option Compare Text


Imports System.Collections.Generic
Imports System.Drawing
Imports Clicknator.ClickinUtil

Public Class Clickinator

#Region " Constants "
    Public Const InstructionSeparator As String = ":"
    Public Const ParameterSeparator As String = ","
    Public Const VariableStartDelemiter As String = "{"
    Public Const VariableEndDelemiter As String = "}"
    Public Const MathStartDelemiter As String = "["
    Public Const MathEndDelemiter As String = "]"
    Public Const DefaultDelay As Long = 50

    Public Const COMMAND_COMMENT As String = "####"
    Public Const COMMAND_MOUSE_MOVE_ABSOLUTE As String = "MABS"
    Public Const COMMAND_MOUSE_MOVE_RELATIVE As String = "MLOC"
    Public Const COMMAND_MOUSE_MOVE_OFFSET As String = "MMOV"
    Public Const COMMAND_MOUSE_CLICK_LEFT As String = "MLCK"
    Public Const COMMAND_MOUSE_CLICK_MIDDLE As String = "MMCK"
    Public Const COMMAND_MOUSE_CLICK_RIGHT As String = "MRCK"
    Public Const COMMAND_MOUSE_DOWN_LEFT As String = "MLDN"
    Public Const COMMAND_MOUSE_DOWN_MIDDLE As String = "MMDN"
    Public Const COMMAND_MOUSE_DOWN_RIGHT As String = "MRDN"
    Public Const COMMAND_MOUSE_UP_LEFT As String = "MLUP"
    Public Const COMMAND_MOUSE_UP_MIDDLE As String = "MMUP"
    Public Const COMMAND_MOUSE_UP_RIGHT As String = "MRUP"

    Public Const COMMAND_NAVIGATE As String = "NAVI"
    Public Const COMMAND_GOTO As String = "GOTO"
    Public Const COMMAND_WAIT As String = "WAIT"
    Public Const COMMAND_MESS As String = "MESS"
    Public Const COMMAND_SETV As String = "SETV"
    Public Const COMMAND_INCV As String = "INCV"
    Public Const COMMAND_DECV As String = "DECV"
    Public Const COMMAND_DO As String = "DOXX"
    Public Const COMMAND_LOOP As String = "LOOP"
#End Region

#Region " Private Members "
    Private InstructionList As New List(Of Instruction)
    Private InstructionIndex As Integer = 0
    Private GetWinLoc As GetWindowLocation
    Private GetCurLoc As GetCursorLocation
    Private DoNav As DoNavigation
    Private ErrorLine As Int32 = 0
    Private UserCancel As Boolean = False
    Private VariablesDictionary As New Dictionary(Of String, String)
    Private ShowMsg As ShowMessage
    Private isSimulationRunning As Boolean = False
    Private SClickMouse As SimulateClickMouse
    Private SMoveMouse As SimulateMoveMouse
    Private doLineBuffer As Int32 = 0
    Private doCountBuffer As Int32 = -1
#End Region

#Region " Delegates "
    Public Delegate Function GetWindowLocation() As Point
    Public Delegate Function GetCursorLocation() As Point
    Public Delegate Sub DoNavigation(ByVal navigationURL As String)
    Public Delegate Sub ShowMessage(ByVal messageText As String)
    Public Delegate Sub SimulateClickMouse(ByVal downButton As Boolean, ByVal buttonNumber As Integer)
    Public Delegate Sub SimulateMoveMouse(ByVal screenLocation As Point)
#End Region

#Region " Constructors "
    ''' <summary>
    ''' Constructor
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub New(ByVal windowloc As GetWindowLocation, ByVal cursorloc As GetCursorLocation, ByVal navig As DoNavigation, ByVal msg As ShowMessage, ByVal simulClick As SimulateClickMouse, ByVal simulMove As SimulateMoveMouse)
        GetWinLoc = windowloc
        GetCurLoc = cursorloc
        DoNav = navig
        ShowMsg = msg
        SClickMouse = simulClick
        SMoveMouse = simulMove
    End Sub
#End Region

#Region " Public Methods "

    ''' <summary>
    ''' Returns a list of all valid commands
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Shared Function ValidCommands() As List(Of String)
        Dim output As New List(Of String)

        output.Add(COMMAND_COMMENT)
        output.Add(COMMAND_MOUSE_MOVE_ABSOLUTE)
        output.Add(COMMAND_MOUSE_MOVE_RELATIVE)
        output.Add(COMMAND_MOUSE_MOVE_OFFSET)
        output.Add(COMMAND_MOUSE_CLICK_LEFT)
        output.Add(COMMAND_MOUSE_CLICK_MIDDLE)
        output.Add(COMMAND_MOUSE_CLICK_RIGHT)
        output.Add(COMMAND_MOUSE_DOWN_LEFT)
        output.Add(COMMAND_MOUSE_DOWN_MIDDLE)
        output.Add(COMMAND_MOUSE_DOWN_RIGHT)
        output.Add(COMMAND_MOUSE_UP_LEFT)
        output.Add(COMMAND_MOUSE_UP_MIDDLE)
        output.Add(COMMAND_MOUSE_UP_RIGHT)
        output.Add(COMMAND_NAVIGATE)
        output.Add(COMMAND_GOTO)
        output.Add(COMMAND_WAIT)
        output.Add(COMMAND_MESS)
        output.Add(COMMAND_SETV)
        output.Add(COMMAND_INCV)
        output.Add(COMMAND_DECV)
        output.Add(COMMAND_DO)
        output.Add(COMMAND_LOOP)

        Return output
    End Function


    ''' <summary>
    ''' This method reads a supplied instruction set and converts it to a collection of macro instructions
    ''' </summary>
    ''' <param name="MacroText"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function ProcessMacro(ByVal MacroText As String) As Boolean
        Dim output As Boolean = True
        Dim instructions As String()
        Dim i As Integer

        instructions = MacroText.Split(Chr(10))
        ErrorLine = 0

        For i = 0 To (instructions.Count - 1)
            Dim instructionItem As String()

            instructionItem = instructions(i).Split(Convert.ToChar(InstructionSeparator))

            If instructionItem.Count <> 2 Then
                'Unexpected number of arguments
                output = False
                ErrorLine = i
                Exit For
            Else
                'add macro to collection
                Dim singleInstruction As Instruction

                'convert to uppercase and strip linefeeds
                singleInstruction.Command = instructionItem(0).ToUpper.Replace(Chr(13), "")

                If (singleInstruction.Command = COMMAND_COMMENT) OrElse (singleInstruction.Command = COMMAND_MESS) Then
                    'dont upper case comments or messages
                    singleInstruction.Param = instructionItem(1)
                Else
                    singleInstruction.Param = instructionItem(1).ToUpper
                End If

                InstructionList.Add(singleInstruction)
            End If

        Next

        Return output
    End Function

    ''' <summary>
    ''' This method advances to the next instruction in the macro
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function MoveNext() As Boolean
        Dim output As Boolean = False

        InstructionIndex += 1

        If InstructionIndex < InstructionList.Count Then
            'still within bounds
            output = True
        End If

        Return output
    End Function

    ''' <summary>
    ''' This method retrives the current instruction in the macro
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetInstruction() As Instruction
        Dim output As Instruction

        If (InstructionIndex >= InstructionList.Count) OrElse (InstructionIndex < 0) Then
            Throw New IndexOutOfRangeException
        Else
            output = InstructionList(InstructionIndex)
        End If

        Return output
    End Function

    ''' <summary>
    ''' This method retrives the current instruction index in the macro
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetInstructionIndex() As Int32
        Return InstructionIndex
    End Function

    ''' <summary>
    ''' This method sets the next instruction to the target index
    ''' </summary>
    ''' <param name="targetIndex"></param>
    ''' <remarks></remarks>
    Public Sub GoToIndex(ByVal targetIndex As Integer)
        If (targetIndex < 0) OrElse (targetIndex >= InstructionList.Count) Then
            Throw New IndexOutOfRangeException
        Else
            'backtrack of 1 since "MoveNext" will increment index by one
            InstructionIndex = (targetIndex - 1)
        End If
    End Sub

    ''' <summary>
    ''' Executes the current instruction
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Execute()
        Execute(GetInstruction)
    End Sub

    ''' <summary>
    ''' Executes a supplied instruction
    ''' </summary>
    ''' <param name="specificInstruction"></param>
    ''' <remarks></remarks>
    Public Sub Execute(ByVal specificInstruction As Instruction)

        Select Case specificInstruction.Command
            Case COMMAND_COMMENT
                'just ignore comments
            Case COMMAND_WAIT
                exWait(specificInstruction.Param)

                'Mouse clicks
            Case COMMAND_MOUSE_CLICK_LEFT
                exClickMouse(specificInstruction.Param, 0)
            Case COMMAND_MOUSE_DOWN_LEFT
                exButtonMouse(True, 0)
            Case COMMAND_MOUSE_UP_LEFT
                exButtonMouse(False, 0)
            Case COMMAND_MOUSE_CLICK_MIDDLE
                exClickMouse(specificInstruction.Param, 1)
            Case COMMAND_MOUSE_DOWN_MIDDLE
                exButtonMouse(True, 1)
            Case COMMAND_MOUSE_UP_MIDDLE
                exButtonMouse(False, 1)
            Case COMMAND_MOUSE_CLICK_RIGHT
                exClickMouse(specificInstruction.Param, 2)
            Case COMMAND_MOUSE_DOWN_RIGHT
                exButtonMouse(True, 2)
            Case COMMAND_MOUSE_UP_RIGHT
                exButtonMouse(False, 2)

                'Mouse movements
            Case COMMAND_MOUSE_MOVE_ABSOLUTE
                exMoveMouse(specificInstruction.Param, 0)
            Case COMMAND_MOUSE_MOVE_RELATIVE
                exMoveMouse(specificInstruction.Param, 1, Nothing, GetWinLoc())
            Case COMMAND_MOUSE_MOVE_OFFSET
                exMoveMouse(specificInstruction.Param, 2, GetCurLoc())

                'variables
            Case COMMAND_SETV
                exSetVariable(specificInstruction.Param)
            Case COMMAND_INCV
                exIncrementVariable(specificInstruction.Param)
            Case COMMAND_DECV
                exDecrementVariable(specificInstruction.Param)

                'miscallenious
            Case COMMAND_MESS
                exMessage(specificInstruction.Param)
            Case COMMAND_NAVIGATE
                DoNav(specificInstruction.Param)

                'code structures
            Case COMMAND_GOTO
                exGoTo(specificInstruction.Param)
            Case COMMAND_DO
                'calculate where to land when looping
                doLineBuffer = (InstructionIndex + 1)
            Case COMMAND_LOOP
                exLoop(specificInstruction.Param)
            Case Else
                'skip over unknown commands
        End Select

        If exWasEscHit() Then
            'user cancelled
            UserCancel = True
        End If
    End Sub

    ''' <summary>
    ''' Enable simulation mode.
    ''' </summary>
    ''' <remarks>Clicks will nto happen and cursor will be a visual representation as well as mouse button state.</remarks>
    Public Sub SimulationStart()
        isSimulationRunning = True
    End Sub

    ''' <summary>
    ''' Disables simulation mode.
    ''' </summary>
    ''' <remarks>Will not actually break the processing; will only disengage simulation.</remarks>
    Public Sub SimulationStop()
        isSimulationRunning = False
    End Sub

    ''' <summary>
    ''' Cancels the simulation
    ''' </summary>
    ''' <remarks></remarks>
    Public Sub Cancel()
        UserCancel = True
    End Sub

#End Region

#Region " Private Methods "

    ''' <summary>
    ''' This utility function parses a string parameter in a valid milissecond delay
    ''' </summary>
    ''' <param name="delayParam"></param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Private Function ParseDelay(ByVal delayParam As String) As Int32
        Dim delay As Int32 = 1

        If IsNumeric(delayParam) Then
            delay = Convert.ToInt32(delayParam)
        End If

        If delay < 1 Then
            delay = 1
        End If

        Return delay
    End Function

    ''' <summary>
    ''' This function evaluates a command text and parses it to replace variable names with their actual values.
    ''' </summary>
    ''' <param name="commandText"></param>
    ''' <returns></returns>
    ''' <remarks>The variable names must be between curly brackets.</remarks>
    Private Function ParseVariables(ByVal commandText As String) As String
        Dim outputText As String = commandText
        Dim individualVariable As String

        For Each individualVariable In VariablesDictionary.Keys
            Dim variableCode As String = String.Concat("{", individualVariable, "}")
            If outputText.Contains(variableCode) Then
                outputText = outputText.Replace(variableCode, VariablesDictionary(individualVariable))
            End If
        Next

        Return outputText
    End Function

    ''' <summary>
    ''' This function evaluates a command text and parses the area within square brackets as a mathematical equation.
    ''' </summary>
    ''' <param name="commandText"></param>
    ''' <returns></returns>
    ''' <remarks>The equation must be between square brackets</remarks>
    Private Function ParseMath(ByVal commandText As String) As String
        Dim output As String = commandText
        Dim mathArray As List(Of String)
        Dim mathExpression As String

        mathArray = ClickinUtil.ExtractDelimitedParameters(commandText, MathStartDelemiter, MathEndDelemiter)

        For Each mathExpression In mathArray
            Dim calculation As New NCalc.Expression(mathExpression.Substring(1, (mathExpression.Length - 2)))

            output = output.Replace(mathExpression, Convert.ToString(calculation.Evaluate))
        Next

        Return output

    End Function


    Private Sub exClickMouse(ByVal delayParam As String, Optional ByVal button As Integer = 0)
        If Me.IsSimulating Then
            'simulate button down and then up to emulate click
            SClickMouse(True, button)
            exWait(delayParam)
            SClickMouse(False, button)
        Else
            ClikinAPI.ClickMouse(ParseDelay(delayParam), button)
        End If

    End Sub

    Private Sub exButtonMouse(ByVal downButton As Boolean, ByVal buttonNumber As Integer)
        If Me.IsSimulating Then
            SClickMouse(downButton, buttonNumber)
        Else
            ClikinAPI.ButtonMouse(downButton, buttonNumber)
        End If
    End Sub

    Private Sub exWait(ByVal delayParam As String)

        ClikinAPI.Sleep(ParseDelay(delayParam))

    End Sub

    Private Sub exMoveMouse(ByVal pointParam As String, ByVal relative As Integer, Optional ByVal curLocation As Point = Nothing, Optional ByVal winLocation As Point = Nothing)
        Dim targetPoint As Point = ConvertToPoint(pointParam)
        Dim calculatedPoint As Point

        'calculate coordinates
        Select Case relative
            Case 0
                'absolute
                calculatedPoint = targetPoint
            Case 1
                'relative to window
                calculatedPoint = AddPoints(winLocation, targetPoint)

            Case 2
                'offset
                calculatedPoint = AddPoints(curLocation, targetPoint)
        End Select

        If Me.IsSimulating Then
            'emulate mouse move
            SMoveMouse(calculatedPoint)
        Else
            'move the mouse
            ClikinAPI.MoveMouse(calculatedPoint.X, calculatedPoint.Y)
        End If

    End Sub

    Private Sub exGoTo(ByVal lineParam As String)
        Dim lineNumber As Integer = CInt(lineParam)
        GoToIndex(lineNumber)
    End Sub


    Private Function exWasEscHit() As Boolean
        Return ClikinAPI.WasKeyHit(ClikinAPI.VK_ESCAPE)
    End Function

    Private Sub exSetVariable(ByVal varParam As String)

        'only evaluate valid parmaters
        If varParam.Contains(",") Then
            Dim splitPoint As Int32 = varParam.IndexOf(",")
            Dim variableName As String
            Dim variableValue As String

            variableName = Trim(varParam.Substring(0, splitPoint))
            variableValue = Trim(varParam.Substring((splitPoint + 1), (Len(varParam) - splitPoint) - 1))

            VariablesDictionary.Add(variableName, variableValue)
        End If

    End Sub

    Private Sub exIncrementVariable(ByVal varParam As String)
        Dim variableName As String = Trim(varParam)

        'only increment existing variables (must be initialized first)
        If VariablesDictionary.ContainsKey(variableName) Then
            If IsNumeric(VariablesDictionary(variableName)) Then
                VariablesDictionary(variableName) = Convert.ToString((Convert.ToInt32(VariablesDictionary(variableName)) + 1))
            End If
        End If

    End Sub

    Private Sub exDecrementVariable(ByVal varParam As String)
        Dim variableName As String = Trim(varParam)

        'only decrement existing variables (must be initialized first)
        If VariablesDictionary.ContainsKey(variableName) Then
            If IsNumeric(VariablesDictionary(variableName)) Then
                VariablesDictionary(variableName) = Convert.ToString((Convert.ToInt32(VariablesDictionary(variableName)) - 1))
            End If
        End If
    End Sub

    Private Sub exMessage(ByVal messageText As String)
        ShowMsg(ParseMath(ParseVariables(messageText)))
    End Sub

    Private Sub exLoop(ByVal varParam As String)
        If String.IsNullOrEmpty(varParam) Then
            'infinite loop; always reset counter to 2
            doCountBuffer = 2
        ElseIf doCountBuffer = -1 Then
            'initialize count buffer the first time
            doCountBuffer = Convert.ToInt32(varParam)
        End If

        'decrement count buffer
        doCountBuffer -= 1

        If doCountBuffer > 0 Then
            'not done yet; continue iterating
            GoToIndex(doLineBuffer)
        Else
            'done iterating; reset counters
            doCountBuffer = -1
            doLineBuffer = 0
        End If
    End Sub

#End Region

#Region " Public Properties "

    ''' <summary>
    ''' Get the last line where a parsing error occured.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property LastErrorLine() As Int32
        Get
            Return ErrorLine
        End Get
    End Property

    ''' <summary>
    ''' Gets the status if the user cancelled the script.
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property UserCancelled() As Boolean
        Get
            Return UserCancel
        End Get
    End Property

    ''' <summary>
    ''' Gets the status if the execution is in simulation mode
    ''' </summary>
    ''' <value></value>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public ReadOnly Property IsSimulating As Boolean
        Get
            Return isSimulationRunning
        End Get
    End Property
#End Region

#Region " Structures "


    Public Structure Instruction
        Public Command As String
        Public Param As String
    End Structure

#End Region

End Class


Public Class RecordSettings
#Region " Declarations "
    Private pCursorSampleRefreshRate As Int32 = 250
    Private pCursorRefreshOnClickOnly As Boolean = False
    Private pCursorPosition As Int32 = CURSOR_POS_ABSOLUTE
    Private pMinimizeWaitTimes As Boolean = False
    Private pMinimizedWaitPeriod As Int32 = 500
#End Region

#Region " Constants "
    Public Const CURSOR_POS_ABSOLUTE As Int32 = 0
    Public Const CURSOR_POS_RELATIVE As Int32 = 1
    Public Const CURSOR_POS_MOVE As Int32 = 2
#End Region

#Region " Properties "
    Public Property CursorSampleRefreshRate() As Int32
        Get
            Return pCursorSampleRefreshRate
        End Get
        Set(ByVal value As Int32)
            pCursorSampleRefreshRate = value
        End Set
    End Property

    Public Property CursorRefreshOnClickOnly() As Boolean
        Get
            Return pCursorRefreshOnClickOnly
        End Get
        Set(ByVal value As Boolean)
            pCursorRefreshOnClickOnly = value
        End Set
    End Property


    Public Property CursorPosition() As Int32
        Get
            Return pCursorPosition
        End Get
        Set(ByVal value As Int32)
            pCursorPosition = value
        End Set
    End Property

    Public Property MinimizeWaitTimes As Boolean
        Get
            Return pMinimizeWaitTimes
        End Get
        Set(value As Boolean)
            pMinimizeWaitTimes = value
        End Set
    End Property

    Public Property MinimizedWaitPeriod As Int32
        Get
            Return pMinimizedWaitPeriod
        End Get
        Set(value As Int32)
            pMinimizedWaitPeriod = value
        End Set
    End Property

#End Region


End Class


Public Class RecordingScript

#Region " Private Members "
    Private pSettings As RecordSettings
    Private InstructionList As New List(Of Clickinator.Instruction)
    Private previousLocation As New Point(0, 0)
    Private previousWait As Date = Date.Now
    Private previousCommand As String = ""
    Private previousLeftDown As Boolean = False
    Private previousMiddleDown As Boolean = False
    Private previousRightDown As Boolean = False

    Private GetWinLoc As GetWindowLocation
#End Region

#Region " Constants "
    Private Const MESS_RECORDER_START As String = "Macro recorder started..."
    Private Const MESS_RECORDER_END As String = "Macro recorder ended..."
#End Region

#Region " Constructors "
    Public Sub New(ByVal settingsForRecording As RecordSettings, ByVal windowLoc As GetWindowLocation)
        pSettings = settingsForRecording
        GetWinLoc = windowLoc

        AddInstruction(Clickinator.COMMAND_COMMENT, MESS_RECORDER_START)
        AddInstruction(Clickinator.COMMAND_COMMENT, DateAndTime.Now.ToString.Replace(":", "."))
    End Sub
#End Region

#Region " Delegates "
    Public Delegate Function GetWindowLocation() As Point
#End Region

#Region " Public Properties "

    Public Property Settings As RecordSettings
        Get
            Return pSettings
        End Get
        Set(value As RecordSettings)
            pSettings = value
        End Set
    End Property

#End Region

#Region " Public Methods "
    Public Sub RecordCursor(ByVal cursorLocation As Point, Optional ByVal forceRecord As Boolean = False)

        'do not record location if set to record on click only
        If (Not pSettings.CursorRefreshOnClickOnly) Or forceRecord Then
            If previousLocation = New Point(0, 0) Then
                'first point

                If pSettings.CursorPosition = RecordSettings.CURSOR_POS_RELATIVE Then
                    'record the relative initial position
                    Dim initialLocation As Point = SubtractPoints(cursorLocation, GetWinLoc())
                    AddInstruction(Clickinator.COMMAND_MOUSE_MOVE_RELATIVE, initialLocation)
                    previousLocation = initialLocation
                    previousCommand = Clickinator.COMMAND_MOUSE_MOVE_RELATIVE
                Else
                    'record the absolute initial position
                    AddInstruction(Clickinator.COMMAND_MOUSE_MOVE_ABSOLUTE, cursorLocation)
                    previousLocation = cursorLocation
                    previousCommand = Clickinator.COMMAND_MOUSE_MOVE_ABSOLUTE
                End If
            Else
                'subsequent point
                Select Case pSettings.CursorPosition
                    Case RecordSettings.CURSOR_POS_MOVE
                        'relative to previous point
                        Dim currentLocation As Point = SubtractPoints(cursorLocation, previousLocation)
                        AddInstruction(Clickinator.COMMAND_MOUSE_MOVE_OFFSET, currentLocation)
                        previousLocation = currentLocation
                        previousCommand = Clickinator.COMMAND_MOUSE_MOVE_OFFSET
                    Case RecordSettings.CURSOR_POS_RELATIVE
                        'relative to window
                        Dim currentLocation As Point = SubtractPoints(cursorLocation, GetWinLoc())
                        AddInstruction(Clickinator.COMMAND_MOUSE_MOVE_RELATIVE, currentLocation)
                        previousLocation = currentLocation
                        previousCommand = Clickinator.COMMAND_MOUSE_MOVE_RELATIVE
                    Case Else
                        'absolute position
                        AddInstruction(Clickinator.COMMAND_MOUSE_MOVE_ABSOLUTE, cursorLocation)
                        previousLocation = cursorLocation
                        previousCommand = Clickinator.COMMAND_MOUSE_MOVE_ABSOLUTE
                End Select
            End If

            'reset wait period after this command
            previousWait = Date.Now
        End If

    End Sub

    Public Sub RecordWaitPeriod()
        Dim waitTime As Int32 = 0
        Dim nowTime As Date

        'calculate interval in MS since last command
        If pSettings.MinimizeWaitTimes Then
            waitTime = pSettings.MinimizedWaitPeriod
        Else
            'waitTime = Convert.ToInt32(nowTime.Subtract(previousWait).TotalMilliseconds)
            waitTime = previousWait.Subtract(nowTime).Milliseconds
        End If

        'previous command was a wait
        If previousCommand = Clickinator.COMMAND_WAIT Then
            'add time to previous wait segment
            InstructionList(InstructionList.Count - 1) = AddToInstruction(InstructionList(InstructionList.Count - 1), waitTime)
        Else
            AddInstruction(Clickinator.COMMAND_WAIT, waitTime.ToString)
        End If

        previousCommand = Clickinator.COMMAND_WAIT
        previousWait = nowTime
    End Sub

    Public Sub DoneRecording()
        AddInstruction(Clickinator.COMMAND_COMMENT, MESS_RECORDER_END)
        AddInstruction(Clickinator.COMMAND_COMMENT, DateAndTime.Now.ToString.Replace(":", "."))
    End Sub

    Public Sub RecordMouseClick(ByVal cursorLocation As Point, ByVal LeftDown As Boolean, ByVal MiddleDown As Boolean, ByVal RightDown As Boolean)

        'if the state of buttons has changed; record the button state
        If Not ((previousLeftDown = LeftDown) And (previousMiddleDown = MiddleDown) And (previousRightDown = RightDown)) Then
            If pSettings.CursorRefreshOnClickOnly Then
                'record cursor location on click if setings indicates
                RecordCursor(cursorLocation, True)
            End If

            If Not (previousLeftDown = LeftDown) Then
                'record left button change
                If LeftDown Then
                    AddInstruction(Clickinator.COMMAND_MOUSE_DOWN_LEFT, String.Empty)
                    previousCommand = Clickinator.COMMAND_MOUSE_DOWN_LEFT
                Else
                    AddInstruction(Clickinator.COMMAND_MOUSE_UP_LEFT, String.Empty)
                    previousCommand = Clickinator.COMMAND_MOUSE_UP_LEFT
                End If
            End If

            If Not (previousMiddleDown = MiddleDown) Then
                'record middle button change
                If MiddleDown Then
                    AddInstruction(Clickinator.COMMAND_MOUSE_DOWN_MIDDLE, String.Empty)
                    previousCommand = Clickinator.COMMAND_MOUSE_DOWN_MIDDLE
                Else
                    AddInstruction(Clickinator.COMMAND_MOUSE_UP_MIDDLE, String.Empty)
                    previousCommand = Clickinator.COMMAND_MOUSE_UP_MIDDLE
                End If
            End If

            If Not (previousRightDown = RightDown) Then
                'record right button change
                If RightDown Then
                    AddInstruction(Clickinator.COMMAND_MOUSE_DOWN_RIGHT, String.Empty)
                    previousCommand = Clickinator.COMMAND_MOUSE_DOWN_RIGHT
                Else
                    AddInstruction(Clickinator.COMMAND_MOUSE_UP_RIGHT, String.Empty)
                    previousCommand = Clickinator.COMMAND_MOUSE_UP_RIGHT
                End If
            End If

            'store previous state
            previousLeftDown = LeftDown
            previousMiddleDown = MiddleDown
            previousRightDown = RightDown

            'reset wait period after this command
            previousWait = Date.Now
        End If

    End Sub

    Public Overrides Function ToString() As String
        Return InstructionListToString(InstructionList)
    End Function


#End Region


#Region " Private Methods "
    Private Sub AddInstruction(ByVal instructionCommand As String, ByVal instructionParameter As String)
        Dim singleInstruction As New Clickinator.Instruction

        singleInstruction.Command = instructionCommand
        singleInstruction.Param = instructionParameter

        InstructionList.Add(singleInstruction)
    End Sub

    Private Sub AddInstruction(ByVal instructionCommand As String, ByVal instructionParameter As Point)
        Dim singleInstruction As New Clickinator.Instruction

        singleInstruction.Command = instructionCommand
        singleInstruction.Param = String.Format("{0},{1}", instructionParameter.X, instructionParameter.Y)

        InstructionList.Add(singleInstruction)
    End Sub

#End Region

End Class